﻿using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;

namespace iStudio.Structure.Control.Audio.DigitalClock
{
    /// <summary>
    /// A digital LED clock display control with 
    /// hours, minutes, seconds, and hundredths of a second.
    /// </summary>
    [DisplayName ("Digital Clock"), Description ("Displays timespans as a digital LED clock."), ToolboxItem (true), TemplatePart (Name = "PART_ClockGrid", Type = typeof (Grid))]
    
    public class DigitalClock : System.Windows.Controls.Control
    {
        #region Fields

        private readonly Rectangle _hourRectangle1 = new Rectangle ();
        private readonly VisualBrush _colonBrush = new VisualBrush ();
        private readonly VisualBrush _decimalBrush = new VisualBrush ();
        private readonly Rectangle _decimalRectangle = new Rectangle ();
        private readonly VisualBrush _eightBrush = new VisualBrush ();
        private readonly VisualBrush _fiveBrush = new VisualBrush ();
        private readonly VisualBrush _fourBrush = new VisualBrush ();
        private readonly Rectangle _hourRectangle2 = new Rectangle ();
        private readonly Rectangle _minuteRectangle1 = new Rectangle ();
        private readonly Rectangle _minuteRectangle2 = new Rectangle ();
        private readonly VisualBrush _nineBrush = new VisualBrush ();
        private readonly VisualBrush _oneBrush = new VisualBrush ();
        private readonly Rectangle _secondRectangle1 = new Rectangle ();
        private readonly Rectangle _secondRectangle2 = new Rectangle ();
        private readonly Rectangle _separatorRectangle1 = new Rectangle ();
        private readonly Rectangle _separatorRectangle2 = new Rectangle ();
        private readonly VisualBrush _sevenBrush = new VisualBrush ();
        private readonly VisualBrush _sixBrush = new VisualBrush ();
        private readonly Rectangle _subSecondRectangle1 = new Rectangle ();
        private readonly Rectangle _subSecondRectangle2 = new Rectangle ();

        private readonly VisualBrush _threeBrush = new VisualBrush ();
        private readonly VisualBrush _twoBrush = new VisualBrush ();
        private readonly VisualBrush _zeroBrush = new VisualBrush ();

        private Grid _clockGrid;

        #endregion

        #region Dependency Properties

        #region Time

        /// <summary>
        /// Identifies the <see cref="Time" /> dependency property. 
        /// </summary>
        public static readonly DependencyProperty TimeProperty = DependencyProperty.Register ("Time", typeof (TimeSpan), typeof (DigitalClock), new UIPropertyMetadata (TimeSpan.Zero, OnTimeChanged, OnCoerceTime));

        /// <summary>
        /// Gets or sets the time to be displayed in the Digital Clock.
        /// </summary>
        public TimeSpan Time
        {
            // IMPORTANT: To maintain parity between setting a property in XAML and procedural code, do not touch the getter and setter inside this dependency property!
            get
            {
                return (TimeSpan) GetValue (TimeProperty);
            }
            set
            {
                SetValue (TimeProperty, value);
            }
        }

        private static object OnCoerceTime (DependencyObject o, object value)
        {
            DigitalClock bigClock = o as DigitalClock;
            if (bigClock != null)
            {
                return bigClock.OnCoerceTime ((TimeSpan) value);
            }
            return value;
        }

        private static void OnTimeChanged (DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            DigitalClock bigClock = o as DigitalClock;
            if (bigClock != null)
            {
                bigClock.OnTimeChanged ((TimeSpan) e.OldValue, (TimeSpan) e.NewValue);
            }
        }

        /// <summary>
        /// Coerces the value of <see cref="Time"/> when a new value is applied.
        /// </summary>
        /// <param name="value">The value that was set on <see cref="Time"/></param>
        /// <returns>The adjusted value of <see cref="Time"/></returns>
        protected virtual TimeSpan OnCoerceTime (TimeSpan value)
        {
            return value;
        }

        /// <summary>
        /// Called after the <see cref="Time"/> value has changed.
        /// </summary>
        /// <param name="oldValue">The previous value of <see cref="Time"/></param>
        /// <param name="newValue">The new value of <see cref="Time"/></param>
        protected virtual void OnTimeChanged (TimeSpan oldValue, TimeSpan newValue)
        {
            SplitDigits ();
        }

        #endregion

        #region ShowHours

        /// <summary>
        /// Identifies the <see cref="ShowHours" /> dependency property. 
        /// </summary>
        public static readonly DependencyProperty ShowHoursProperty = DependencyProperty.Register ("ShowHours", typeof (bool), typeof (DigitalClock), new UIPropertyMetadata (true, OnShowHoursChanged, OnCoerceShowHours));

        /// <summary>
        /// Gets or sets whether the digital clock will show the hours portion
        /// in the digital clock display. This is useful if the times displayed
        /// are always less than an hour.
        /// </summary>
        public bool ShowHours
        {
            // IMPORTANT: To maintain parity between setting a property in XAML and procedural code, do not touch the getter and setter inside this dependency property!
            get
            {
                return (bool) GetValue (ShowHoursProperty);
            }
            set
            {
                SetValue (ShowHoursProperty, value);
            }
        }

        private static object OnCoerceShowHours (DependencyObject o, object value)
        {
            DigitalClock digitalClock = o as DigitalClock;
            if (digitalClock != null)
            {
                return digitalClock.OnCoerceShowHours ((bool) value);
            }
            return value;
        }

        private static void OnShowHoursChanged (DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            DigitalClock digitalClock = o as DigitalClock;
            if (digitalClock != null)
            {
                digitalClock.OnShowHoursChanged ((bool) e.OldValue, (bool) e.NewValue);
            }
        }

        /// <summary>
        /// Coerces the value of <see cref="ShowHours"/> when a new value is applied.
        /// </summary>
        /// <param name="value">The value that was set on <see cref="ShowHours"/></param>
        /// <returns>The adjusted value of <see cref="ShowHours"/></returns>
        protected virtual bool OnCoerceShowHours (bool value)
        {
            return value;
        }

        /// <summary>
        /// Called after the <see cref="ShowHours"/> value has changed.
        /// </summary>
        /// <param name="oldValue">The previous value of <see cref="ShowHours"/></param>
        /// <param name="newValue">The new value of <see cref="ShowHours"/></param>
        protected virtual void OnShowHoursChanged (bool oldValue, bool newValue)
        {
            FormatClockLayout ();
            SplitDigits ();
        }

        #endregion

        #region ShowSubSeconds

        /// <summary>
        /// Identifies the <see cref="ShowSubSeconds" /> dependency property. 
        /// </summary>
        public static readonly DependencyProperty ShowSubSecondsProperty = DependencyProperty.Register ("ShowSubSeconds", typeof (bool), typeof (DigitalClock), new UIPropertyMetadata (false, OnShowSubSecondsChanged, OnCoerceShowSubSeconds));

        /// <summary>
        /// Gets or sets whether fractions of a second are displayed in the digital
        /// clock display.
        /// </summary>
        public bool ShowSubSeconds
        {
            // IMPORTANT: To maintain parity between setting a property in XAML and procedural code, do not touch the getter and setter inside this dependency property!
            get
            {
                return (bool) GetValue (ShowSubSecondsProperty);
            }
            set
            {
                SetValue (ShowSubSecondsProperty, value);
            }
        }

        private static object OnCoerceShowSubSeconds (DependencyObject o, object value)
        {
            DigitalClock digitalClock = o as DigitalClock;
            if (digitalClock != null)
            {
                return digitalClock.OnCoerceShowSubSeconds ((bool) value);
            }
            return value;
        }

        private static void OnShowSubSecondsChanged (DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            DigitalClock digitalClock = o as DigitalClock;
            if (digitalClock != null)
            {
                digitalClock.OnShowSubSecondsChanged ((bool) e.OldValue, (bool) e.NewValue);
            }
        }

        /// <summary>
        /// Coerces the value of <see cref="ShowSubSeconds"/> when a new value is applied.
        /// </summary>
        /// <param name="value">The value that was set on <see cref="ShowSubSeconds"/></param>
        /// <returns>The adjusted value of <see cref="ShowSubSeconds"/></returns>
        protected virtual bool OnCoerceShowSubSeconds (bool value)
        {
            return value;
        }

        /// <summary>
        /// Called after the <see cref="ShowSubSeconds"/> value has changed.
        /// </summary>
        /// <param name="oldValue">The previous value of <see cref="ShowSubSeconds"/></param>
        /// <param name="newValue">The new value of <see cref="ShowSubSeconds"/></param>
        protected virtual void OnShowSubSecondsChanged (bool oldValue, bool newValue)
        {
            FormatClockLayout ();
            SplitDigits ();
        }

        #endregion

        #endregion

        #region Constructors

        static DigitalClock ()
        {
            DefaultStyleKeyProperty.OverrideMetadata (typeof (DigitalClock), new FrameworkPropertyMetadata (typeof (DigitalClock)));
        }

        #endregion

        #region Template Overrides

        /// <summary>
        /// When overridden in a derived class, is invoked whenever application code
        /// or internal processes call System.Windows.FrameworkElement.ApplyTemplate().
        /// </summary>
        public override void OnApplyTemplate ()
        {
            base.OnApplyTemplate ();

            if (_clockGrid != null)
            {
                _clockGrid.Children.Clear ();
            }

            _clockGrid = GetTemplateChild ("PART_ClockGrid") as Grid;

            _oneBrush.Visual = FindResource ("One") as Visual;
            _oneBrush.Stretch = Stretch.Uniform;
            _oneBrush.AlignmentY = AlignmentY.Center;
            RenderOptions.SetCachingHint (_oneBrush, CachingHint.Cache);
            _twoBrush.Visual = FindResource ("Two") as Visual;
            _twoBrush.Stretch = Stretch.Uniform;
            _twoBrush.AlignmentY = AlignmentY.Center;
            RenderOptions.SetCachingHint (_twoBrush, CachingHint.Cache);
            _threeBrush.Visual = FindResource ("Three") as Visual;
            _threeBrush.Stretch = Stretch.Uniform;
            _threeBrush.AlignmentY = AlignmentY.Center;
            RenderOptions.SetCachingHint (_threeBrush, CachingHint.Cache);
            _fourBrush.Visual = FindResource ("Four") as Visual;
            _fourBrush.Stretch = Stretch.Uniform;
            _fourBrush.AlignmentY = AlignmentY.Center;
            RenderOptions.SetCachingHint (_fourBrush, CachingHint.Cache);
            _fiveBrush.Visual = FindResource ("Five") as Visual;
            _fiveBrush.Stretch = Stretch.Uniform;
            _fiveBrush.AlignmentY = AlignmentY.Center;
            RenderOptions.SetCachingHint (_fiveBrush, CachingHint.Cache);
            _sixBrush.Visual = FindResource ("Six") as Visual;
            _sixBrush.Stretch = Stretch.Uniform;
            _sixBrush.AlignmentY = AlignmentY.Center;
            RenderOptions.SetCachingHint (_sixBrush, CachingHint.Cache);
            _sevenBrush.Visual = FindResource ("Seven") as Visual;
            _sevenBrush.Stretch = Stretch.Uniform;
            _sevenBrush.AlignmentY = AlignmentY.Center;
            RenderOptions.SetCachingHint (_sevenBrush, CachingHint.Cache);
            _eightBrush.Visual = FindResource ("Eight") as Visual;
            _eightBrush.Stretch = Stretch.Uniform;
            _eightBrush.AlignmentY = AlignmentY.Center;
            RenderOptions.SetCachingHint (_eightBrush, CachingHint.Cache);
            _nineBrush.Visual = FindResource ("Nine") as Visual;
            _nineBrush.Stretch = Stretch.Uniform;
            _nineBrush.AlignmentY = AlignmentY.Center;
            RenderOptions.SetCachingHint (_nineBrush, CachingHint.Cache);
            _zeroBrush.Visual = FindResource ("Zero") as Visual;
            _zeroBrush.Stretch = Stretch.Uniform;
            _zeroBrush.AlignmentY = AlignmentY.Center;
            RenderOptions.SetCachingHint (_zeroBrush, CachingHint.Cache);
            _decimalBrush.Visual = FindResource ("Decimal") as Visual;
            _decimalBrush.Stretch = Stretch.Uniform;
            _decimalBrush.AlignmentY = AlignmentY.Bottom;
            RenderOptions.SetCachingHint (_decimalBrush, CachingHint.Cache);
            _colonBrush.Visual = FindResource ("Colon") as Visual;
            _colonBrush.Stretch = Stretch.Uniform;
            _colonBrush.AlignmentY = AlignmentY.Center;
            RenderOptions.SetCachingHint (_colonBrush, CachingHint.Cache);

            _separatorRectangle1.Fill = GetDigitElement (':');
            _separatorRectangle2.Fill = GetDigitElement (':');
            _decimalRectangle.Fill = GetDigitElement ('.');

            FormatClockLayout ();
            SplitDigits ();
        }

        #endregion

        #region Private Utility Methods

        private void FormatClockLayout ()
        {
            if (_clockGrid == null)
            {
                return;
            }

            ColumnDefinitionCollection clockColumns = _clockGrid.ColumnDefinitions;
            clockColumns.Clear ();
            _clockGrid.Children.Clear ();

            Thickness digitMargin = new Thickness (5.0d);
            int columnIndex = 0;
            double gridLength = 0;
            const double gridHeight = 115;

            if (ShowHours)
            {
                // First hours digit
                ColumnDefinition hours1Column = new ColumnDefinition
                                                    {
                                                        Width = new GridLength (50, GridUnitType.Star)
                                                    };
                clockColumns.Add (hours1Column);
                _hourRectangle1.Margin = digitMargin;
                Grid.SetColumn (_hourRectangle1, columnIndex);
                _clockGrid.Children.Add (_hourRectangle1);
                gridLength += 60;
                columnIndex++;

                // Second hours digit
                ColumnDefinition hours2Column = new ColumnDefinition
                                                    {
                                                        Width = new GridLength (50, GridUnitType.Star)
                                                    };
                clockColumns.Add (hours2Column);
                _hourRectangle2.Margin = digitMargin;
                Grid.SetColumn (_hourRectangle2, columnIndex);
                _clockGrid.Children.Add (_hourRectangle2);
                gridLength += 60;
                columnIndex++;

                // Hours separator
                ColumnDefinition seaparator1Column = new ColumnDefinition
                                                         {
                                                             Width = new GridLength (20, GridUnitType.Star)
                                                         };
                clockColumns.Add (seaparator1Column);
                _separatorRectangle1.Margin = digitMargin;
                Grid.SetColumn (_separatorRectangle1, columnIndex);
                _clockGrid.Children.Add (_separatorRectangle1);
                gridLength += 30;
                columnIndex++;
            }

            // First minutes digit            
            ColumnDefinition minutes1Column = new ColumnDefinition
                                                  {
                                                      Width = new GridLength (50, GridUnitType.Star)
                                                  };
            clockColumns.Add (minutes1Column);
            _minuteRectangle1.Margin = digitMargin;
            Grid.SetColumn (_minuteRectangle1, columnIndex);
            _clockGrid.Children.Add (_minuteRectangle1);
            gridLength += 60;
            columnIndex++;

            // Second minutes digit
            ColumnDefinition minutes2Column = new ColumnDefinition
                                                  {
                                                      Width = new GridLength (50, GridUnitType.Star)
                                                  };
            clockColumns.Add (minutes2Column);
            _minuteRectangle2.Margin = digitMargin;
            Grid.SetColumn (_minuteRectangle2, columnIndex);
            _clockGrid.Children.Add (_minuteRectangle2);
            gridLength += 60;
            columnIndex++;

            // Minutes separator
            ColumnDefinition seaparator2Column = new ColumnDefinition
                                                     {
                                                         Width = new GridLength (20, GridUnitType.Star)
                                                     };
            clockColumns.Add (seaparator2Column);
            _separatorRectangle2.Margin = digitMargin;
            Grid.SetColumn (_separatorRectangle2, columnIndex);
            _clockGrid.Children.Add (_separatorRectangle2);
            gridLength += 30;
            columnIndex++;

            // First seconds digit
            ColumnDefinition seconds1Column = new ColumnDefinition
                                                  {
                                                      Width = new GridLength (50, GridUnitType.Star)
                                                  };
            clockColumns.Add (seconds1Column);
            _secondRectangle1.Margin = digitMargin;
            Grid.SetColumn (_secondRectangle1, columnIndex);
            _clockGrid.Children.Add (_secondRectangle1);
            gridLength += 60;
            columnIndex++;

            // Second seconds digit
            ColumnDefinition seconds2Column = new ColumnDefinition
                                                  {
                                                      Width = new GridLength (50, GridUnitType.Star)
                                                  };
            clockColumns.Add (seconds2Column);
            _secondRectangle2.Margin = digitMargin;
            Grid.SetColumn (_secondRectangle2, columnIndex);
            _clockGrid.Children.Add (_secondRectangle2);
            gridLength += 60;
            columnIndex++;

            if (ShowSubSeconds)
            {
                // Subseconds decimal point
                ColumnDefinition subSecondsDecimalColumn = new ColumnDefinition
                                                               {
                                                                   Width = new GridLength (20, GridUnitType.Star)
                                                               };
                clockColumns.Add (subSecondsDecimalColumn);
                _decimalRectangle.Margin = digitMargin;
                Grid.SetColumn (_decimalRectangle, columnIndex);
                _clockGrid.Children.Add (_decimalRectangle);
                gridLength += 30;
                columnIndex++;

                // First subseconds digit
                ColumnDefinition subSeconds1Column = new ColumnDefinition
                                                         {
                                                             Width = new GridLength (50, GridUnitType.Star)
                                                         };
                clockColumns.Add (subSeconds1Column);
                _subSecondRectangle1.Margin = digitMargin;
                Grid.SetColumn (_subSecondRectangle1, columnIndex);
                _clockGrid.Children.Add (_subSecondRectangle1);
                gridLength += 60;
                columnIndex++;

                // Second subseconds digit
                ColumnDefinition subSeconds2Column = new ColumnDefinition
                                                         {
                                                             Width = new GridLength (50, GridUnitType.Star)
                                                         };
                clockColumns.Add (subSeconds2Column);
                _subSecondRectangle2.Margin = digitMargin;
                Grid.SetColumn (_subSecondRectangle2, columnIndex);
                _clockGrid.Children.Add (_subSecondRectangle2);
                gridLength += 60;
            }

            _clockGrid.Width = gridLength;
            _clockGrid.Height = gridHeight;
        }

        private void SplitDigits ()
        {
            if (_clockGrid == null)
            {
                return;
            }

            const string timeFormat = "00";

            string hoursString = Time.Hours.ToString (timeFormat);
            _hourRectangle1.Fill = GetDigitElement (hoursString[0]);
            _hourRectangle2.Fill = GetDigitElement (hoursString[1]);

            string minutesString = Time.Minutes.ToString (timeFormat);
            _minuteRectangle1.Fill = GetDigitElement (minutesString[0]);
            _minuteRectangle2.Fill = GetDigitElement (minutesString[1]);

            string secondsString = Time.Seconds.ToString (timeFormat);
            _secondRectangle1.Fill = GetDigitElement (secondsString[0]);
            _secondRectangle2.Fill = GetDigitElement (secondsString[1]);

            string subSecondsString = (Time.Milliseconds / 1000.0d).ToString ("0.00");
            _subSecondRectangle1.Fill = GetDigitElement (subSecondsString[2]);
            _subSecondRectangle2.Fill = GetDigitElement (subSecondsString[3]);
        }

        private VisualBrush GetDigitElement (char digitChar)
        {
            switch (digitChar)
            {
                case '1':
                    return _oneBrush;
                case '2':
                    return _twoBrush;
                case '3':
                    return _threeBrush;
                case '4':
                    return _fourBrush;
                case '5':
                    return _fiveBrush;
                case '6':
                    return _sixBrush;
                case '7':
                    return _sevenBrush;
                case '8':
                    return _eightBrush;
                case '9':
                    return _nineBrush;
                case '0':
                    return _zeroBrush;
                case '.':
                    return _decimalBrush;
                case ':':
                    return _colonBrush;
            }
            return null;
        }

        #endregion
    }
}